﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using com.ddmresources.EnumMetadata.Core.Attributes;
using Newtonsoft.Json;

namespace com.ddmresources.EnumMetadata.Core
{
    /// <summary>
    /// This class provides extension and static methods to enable custom, strongly typed metadata objects to be associated with enum instances.
    /// </summary>
    public static class EnumMetadataHelper
    {
        #region Constants
        public static readonly int InvalidEnumValueReplacementValue = -1;
        #endregion

        #region Extension Methods
        /// <summary>
        /// An extension method that is passed an enum instance and the type of metadata object that is associated with the this enum, and then
        /// returns the specific metadata object instance associated with that enum instance. This method requires that the enum be attributed
        /// with the EnumInfoSpecification attribute, and that the enum instance be attributed with an attribute that can be resolved into
        /// a metadata object of the passed in type.
        /// </summary>
        /// <typeparam name="T">The type of the returned metadata object</typeparam>
        /// <param name="enumInstance">The instance of enum type (a specific value from the list of values an enum defines) </param>
        /// <returns>The metadata object of type T that is associated with the passed in enum instance</returns>
        public static T Info<T>(this Enum enumInstance)
        {
            T info = default(T);

            if (enumInstance == null){throw new ArgumentException("Parameter Enum enumInstance may not be null");}

            //find handler
            var enumInfoSpecificationAttribute = GetEnumDefinitionAttribute<EnumInfoSpecificationAttribute>(enumInstance);

            VerifyEnumInfoSpecificationAttribute(enumInfoSpecificationAttribute);

            //now that the enumInfoSpecificationAttribute is verified, proceed with the actual logic
            switch (enumInfoSpecificationAttribute.InfoMode)
            {
                case InfoModeOptions.Static: //the attribute directly contains the metadata
                    info = (T) GetEnumInstanceAttribute(enumInstance, enumInfoSpecificationAttribute.OutputType);
                    break;

                case InfoModeOptions.Dynamic:
                    //the attribute contains JSON that needs to be deserialized into the right container type
                    info = GetEnumDynamicInfo<T>(enumInstance);
                    break;
                default:
                    //currently no action needed because VerifyEnumInfoSpecificationAttribute will throw if an invalid InfoModeOption is specified
                    break;
            }

            return info;
        }

        /// <summary>
        /// An extension method that is passed an enum instance and the type of metadata object that is associated with the this enum, and then
        /// returns the specific metadata object instance associated with that enum instance. This method requires that the enum be attributed
        /// with the EnumInfoSpecification attribute, and that the enum instance be attributed with an attribute that can be resolved into
        /// a metadata object of the passed in type.
        /// 
        /// This version of the method is permissive, and will return an new (blank/empty) instance of whatever type T was specified
        /// even if a null enumInstance is passed in, or if the enumInstance isn't a valid member of the enum, or if any of the
        /// required attributes are missing.
        /// </summary>
        /// <typeparam name="T">The type of the returned metadata object</typeparam>
        /// <param name="enumInstance">The instance of enum type (a specific value from the list of values an enum defines) </param>
        /// <returns>The metadata object of type T that is associated with the passed in enum instance</returns>
        public static T InfoLax<T>(this Enum enumInstance) where T: class, new()
        {
            T info = default(T);

            if (enumInstance == null)
            {
                info = new T();
            }
            else
            {
                //find handler
                var enumInfoSpecificationAttribute = GetEnumDefinitionAttribute<EnumInfoSpecificationAttribute>(enumInstance);

                VerifyEnumInfoSpecificationAttribute(enumInfoSpecificationAttribute);

                //now that the enumInfoSpecificationAttribute is verified, proceed with the actual logic
                switch (enumInfoSpecificationAttribute.InfoMode)
                {
                    case InfoModeOptions.Static: //the attribute directly contains the metadata
                        info = (T)GetEnumInstanceAttributeLax(enumInstance, enumInfoSpecificationAttribute.OutputType);
                        break;

                    case InfoModeOptions.Dynamic:
                        if (System.Convert.ToInt32(enumInstance) == InvalidEnumValueReplacementValue)
                        {
                            info = new T();
                        }
                        else //good data, proceed
                        {
                            //the attribute contains JSON that needs to be deserialized into the right container type
                            info = GetEnumDynamicInfo<T>(enumInstance);
                        }

                        break;
                    default:
                        //currently no action needed because VerifyEnumInfoSpecificationAttribute will throw if an invalid InfoModeOption is specified
                        break;
                }
            }
          

            return info;
        }
        #endregion

        #region Public Static Methods
        /// <summary>
        /// This method retrieves the first attribute of the specified type that is applied to the passed in enum instance.
        /// 
        /// This is basically a convenience method that allows the caller to get the output of GetSingleMemberAttribute without
        /// having to cast the enum instance to a MemberInfo object, and without having to harvest just the first attribute instance out
        /// of the many that could potentially be returned. This method is useful when you don't know exactly what enum type will be passed in,
        /// but you do know that it will be attributed with a certain metadata type.
        /// 
        /// This version of the method is strict and will throw ArgumentException if enumInstance is null, soughtAttributeType is null, or
        /// the enumInstance isn't a valid member of the enum.
        /// </summary>
        /// <param name="enumInstance">The enum instance from which the caller wishes to retrieve the requested attribute</param>
        /// <param name="soughtAttributeType">The type of attribute which the caller wishes to retrieve from the specified enum instance</param>
        /// <returns>The first attribute of the specified type that is applied to the specified enum instance. The attribute instance is weakly typed as object.</returns>
        public static object GetEnumInstanceAttribute(Enum enumInstance, Type soughtAttributeType)
        {
            if (enumInstance == null || soughtAttributeType == null)
            {
                throw new ArgumentException("Parameters Enum enumInstance and Type soughtAttributeType must both be assigned non-null values.");
            }

            FieldInfo fi = enumInstance.GetType().GetField(enumInstance.ToString());

            object attribute = GetSingleMemberAttribute(fi, soughtAttributeType);

            return attribute;
        }

        /// <summary>
        /// This method retrieves the first attribute of the specified type that is applied to the passed in enum instance.
        /// 
        /// This is basically a convenience method that allows the caller to get the output of GetSingleMemberAttribute without
        /// having to cast the enum instance to a MemberInfo object, and without having to harvest just the first attribute instance out
        /// of the many that could potentially be returned. This method is useful when you don't know exactly what enum type will be passed in,
        /// but you do know that it will be attributed with a certain metadata type.
        /// 
        /// This version of the method is permissive, and will return an new (blank/empty) instance of whatever soughtAttributeType type was specified
        /// even if a null enumInstance is passed in, or if the enumInstance isn't a valid member of the enum. A null soughtAttributeType still throws 
        /// ArgumentException.
        /// </summary>
        /// <param name="enumInstance">The enum instance from which the caller wishes to retrieve the requested attribute</param>
        /// <param name="soughtAttributeType">The type of attribute which the caller wishes to retrieve from the specified enum instance</param>
        /// <returns>The first attribute of the specified type that is applied to the specified enum instance. The attribute instance is weakly typed as object.</returns>
        public static object GetEnumInstanceAttributeLax(Enum enumInstance, Type soughtAttributeType)
        {
            object attribute = null;

            if (soughtAttributeType == null)
            {
                throw new ArgumentException("Parameter Type soughtAttributeType must be assigned a non-null value.");
            }


            if (enumInstance != null)
            {
                FieldInfo fi = enumInstance.GetType().GetField(enumInstance.ToString());
                if (fi != null)
                {
                    attribute = GetSingleMemberAttribute(fi, soughtAttributeType);
                }
                else
                {
                    attribute = Activator.CreateInstance(soughtAttributeType);
                }
            }
            else
            {
                attribute = Activator.CreateInstance(soughtAttributeType);
            }

            return attribute;
        }

        /// <summary>
        /// Returns the enumeration instance of the passed in enum type
        /// that has the passed in (int) enumValue.
        /// 
        /// If a value is passed in that is not defined in the enum, this method will return an enum instance with
        /// the marker, or sentinal, value of -1. You can check specifically for this value to determine whether a match was found,
        /// using the exposed constant of InvalidEnumValueReplacementValue, or you can use code like this:
        /// bool isValueDefined = Enum.IsDefined(typeof(T), enumValue); 
        /// 
        /// </summary>
        /// <typeparam name="T">The type of the enum from which an instance will be returned</typeparam>
        /// <param name="enumValue">The int value used to match an enum instance that has that int as its value</param>
        /// <returns>An enum instance (of the enum of type T) whose value is the passed in int</returns>
        public static T GetEnumInstance<T>(int enumValue)
        {
            T enumInstance;

            bool isValueDefined = Enum.IsDefined(typeof(T), enumValue);

            if (isValueDefined)
            {
                enumInstance = (T)Enum.ToObject(typeof(T), enumValue);
            }
            else
            {
                enumInstance = (T)Enum.ToObject(typeof(T), InvalidEnumValueReplacementValue);
            }

            return enumInstance;

        }

        /// <summary>
        /// Returns the enumeration instance of the passed in enum type
        /// that has the passed in (string) enumValue.
        /// 
        /// If a value is passed in that is not defined in the enum, this method will return an enum instance with
        /// the marker, or sentinal, value of -1. You can check specifically for this value to determine whether a match was found,
        /// using the exposed constant of InvalidEnumValueReplacementValue.
        /// </summary>
        /// <typeparam name="T">The type of the enum from which an instance will be returned</typeparam>
        /// <param name="enumValue">The string value used to match an enum instance that has that staring as its identifier</param>
        /// <returns>An enum instance (of the enum of type T) whose identifier is the passed in string</returns>
        public static T GetEnumInstance<T>(string enumValue) where T: struct
        {
            T enumInstance;

            if (!Enum.TryParse(enumValue, true, out enumInstance))
            {
                enumInstance = (T)Enum.ToObject(typeof(T), InvalidEnumValueReplacementValue);
            }
           
            return enumInstance;
        }

        /// <summary>
        /// This method performs a reverse lookup of sorts. When provided an enum, a metadata type, an expression resolving to a field
        /// on the metadata type and a search value, this method walks through all the instance members of the enum, looking for all the
        /// members whose associated metadata object contains the search value in the specified field.
        /// 
        /// This method will always return an IEnumerable containing the enum instances whose metadata objects contain the sought data. If there were no
        /// enum instances whose metadata objects contain the sought data, an emtpy IEnumerable will be returned.
        /// </summary>
        /// <typeparam name="T">The type of the enum from which instances may be returned</typeparam>
        /// <typeparam name="I">The type of the interface that is implemented by the strongly typed metadata object associated with the enum instances</typeparam>
        /// <typeparam name="S">The type of the value for which a matching value is sought in the metadata object</typeparam>
        /// <param name="field">An expression that resolves to a property on the associated metadata object</param>
        /// <param name="valueToMatch">The actual value for which associated metadata objects are inspected to find a match</param>
        /// <returns>IEnumerable containing the enum instances whose metadata objects contain the sought data</returns>
        public static IEnumerable<T> FindEnumInstances<T, I, S>(Expression<Func<S>> field, S valueToMatch) where I : class
        {
            if (field == null)
            {
                throw new ArgumentException(
                    "A lamba expression must be specified to indicate which fields of the associated info object should be checked for the search value");
            }

            var foundInstances = new List<T>(); //0 elements to start with, and this will stay at 0 entries if no match is found

            string propertyName = ReflectionUtil.GetProperty(field).Name;

            //for each enum instance of the enum type (until we get a match) 
            foreach (Enum enumInstance in Enum.GetValues(typeof(T)))
            {
                //first, we need to get the extended data associated with this instance of the enum
                var info = enumInstance.Info<I>();

                if (info != null)
                {
                    //now use reflection to call the named property on the extended data object, and see if the result matches the passed in valueToMatch
                    PropertyInfo propertyInfo = (info.GetType()).GetProperty(propertyName);

                    if (propertyInfo != null)
                    {
                        var propertyActualValue = (S)propertyInfo.GetValue(info, null);

                        if (propertyActualValue != null)
                        {
                            if (propertyActualValue.Equals(valueToMatch))
                            {
                                var foundEnumInstance = (T)Enum.ToObject(typeof(T), enumInstance);
                                foundInstances.Add(foundEnumInstance);
                            }
                        }
                    }
                }
            }
            return foundInstances;
        }

        /// <summary>
        /// This method takes an enum type and returns a list of KeyValuePair whose key is an instance of the enum, and 
        /// whose value is specified by the passed in expression which resolves to a property on the metadata object associated with each enum instance.
        /// 
        /// This method is a pass through to the more general method that can take any IEnumerable of enum instances (including filtered lists). In this
        /// case, all of the enum members/instances are extracted from the passed in enum, and used as the source list for the more general method
        /// 
        /// This output is very convenient to use as the data source for dropdown lists.
        /// </summary>
        /// <typeparam name="T">The type of the enum that will be used as the source of enum instances and data for the KeyValuePairs</typeparam>
        /// <typeparam name="I">The type of the interface that is implemented by the strongly typed metadata object associated with the enum instances</typeparam>
        /// <typeparam name="S">The type of the value for which a matching value is sought in the metadata object</typeparam>
        /// <param name="nameField">An expression that resolves to a property on the associated metadata object</param>
        /// <returns>A list of KeyValuePair objects whose key is an instance of the enum, and 
        /// whose value is the appropriate metadata.</returns>
        public static List<KeyValuePair<T, string>> ExtractEnumList<T, I, S>(Expression<Func<S>> nameField) where I : class
        {       
            //turn the full list of enum instances for the given enum into an IEnumerable
            var allEnumInstances = (IEnumerable<T>)Enum.GetValues(typeof(T));

            var extractedList = ExtractEnumList<T, I, S>(nameField, allEnumInstances);

            return extractedList;
        }


        /// <summary>
        /// This method takes an IEnumerable of enum instances and returns a list of KeyValuePair whose key is one of the enum instances, and 
        /// whose value is specified by the passed in expression which resolves to a property on the metadata object associated with each enum instance.
        /// 
        /// This output is very convenient to use as the data source for dropdown lists.
        /// </summary>
        /// <typeparam name="T">The type of the enum that will be used as the source of enum instances and data for the KeyValuePairs</typeparam>
        /// <typeparam name="I">The type of the interface that is implemented by the strongly typed metadata object associated with the enum instances</typeparam>
        /// <typeparam name="S">The type of the value for which a matching value is sought in the metadata object</typeparam>
        /// <param name="nameField">An expression that resolves to a property on the associated metadata object</param>
        /// <param name="enumInstances">An IEnumerable of enum instances that are the source from which the output
        /// list of KeyValuePairs is constructed</param>
        /// <returns>A list of KeyValuePair objects whose key is an instance of the enum, and 
        /// whose value is the appropriate metadata.</returns>
        public static List<KeyValuePair<T, string>> ExtractEnumList<T, I, S>(Expression<Func<S>> nameField, IEnumerable<T> enumInstances) where I : class
        {
            var extractedList = new List<KeyValuePair<T, string>>();

            string propertyName = ReflectionUtil.GetProperty(nameField).Name;

            foreach (var enumInstanceIterator in enumInstances)
            {
                var enumInstance = enumInstanceIterator as Enum;

                //first, we need to get the extended data associated with this instance of the enum
                var info = enumInstance.Info<I>();

                if (info != null)
                {
                    //now use reflection to call the named property on the extended data object, and see if the result matches the passed in valueToMatch
                    PropertyInfo propertyInfo = (info.GetType()).GetProperty(propertyName);

                    if (propertyInfo != null)
                    {
                        var propertyActualValue = (S)propertyInfo.GetValue(info, null);

                        extractedList.Add(new KeyValuePair<T, string>((T)Enum.ToObject(typeof(T), enumInstance), propertyActualValue.ToString()));
                    }
                }
            }

            return extractedList;
        }
        #endregion

        #region Utility Methods

        /// <summary>
        /// Most of the public methods in this class require that the enum to be extended be attributed with the EnumInfoSpecificationAttribute. This
        /// method asserts that the EnumInfoSpecificationAttribute (which has been retrieved using other utility methods) is not null, and that
        /// it's important properties have valid, non-null values.
        /// </summary>
        /// <param name="enumInfoSpecificationAttribute">The EnumInfoSpecificationAttribute instance that needs to be validated </param>
        private static void VerifyEnumInfoSpecificationAttribute(EnumInfoSpecificationAttribute enumInfoSpecificationAttribute)
        {
            if (enumInfoSpecificationAttribute == null)
            {
                throw new InvalidOperationException(
                    "'Info' extension method cannot be invoked on an enum unless that enum has been attributed with EnumInfoSpecificationAttribute");
            }

            if (enumInfoSpecificationAttribute.MetadataAttributeType == null || enumInfoSpecificationAttribute.OutputType == null)
            {
                throw new ArgumentException("EnumInfoSpecificationAttribute must have non-null values provided for the MetadataAttributeType and OutputType properties");
            }

            if (!Enum.IsDefined(typeof(InfoModeOptions), enumInfoSpecificationAttribute.InfoMode))
            {
                throw new ArgumentException("Unknown InfoMode specified for the EnumInfoSpecificationAttribute.");
            }
        }

        /// <summary>
        /// This method retrieves the EnumDynamicInfoAttribute from enums that are storing their metadata in dynamic format (as JSON), pulls
        /// out the JSON content (the actual metadata) from that attribute and deserializes the metadata back into the specified type of metadata object.
        /// </summary>
        /// <typeparam name="T">The type of the interface that is implemented by the strongly typed metadata object associated with the enum instances</typeparam>
        /// <param name="enumInstance">The instance of enum type (a specific value from the list of values an enum defines) whose associated dynamic
        /// metadata is being requested</param>
        /// <returns>The metadata object of type T that is associated with the passed in enum instance</returns>
        private static dynamic GetEnumDynamicInfo<T>(Enum enumInstance)
        {
            if (enumInstance == null){throw new ArgumentException("Parameter Enum enumInstance may not be null");}

            var enumInfoSpecificationAttribute = GetEnumDefinitionAttribute<EnumInfoSpecificationAttribute>(enumInstance);

            VerifyEnumInfoSpecificationAttribute(enumInfoSpecificationAttribute);

            //get dynamic info attribute, if any exists:
            var metadataAttribute = GetEnumInstanceAttribute<EnumDynamicInfoAttribute>(enumInstance);

            if (metadataAttribute == null)
            {
                throw new InvalidOperationException(
                    "The enum instance must be attributed with the EnumDynamicInfo attribute when the enum definition has an EnumInfoSpecification attribute that specifies InfoModeOptions.Dynamic as the metadata source");
            }

            object deserializedOutput = JsonConvert.DeserializeObject(metadataAttribute.Info, enumInfoSpecificationAttribute.OutputType);

            var result = (T) deserializedOutput;

            return result;
        }
 
        #endregion

        #region Attribute Access Methods
        //all these methods can return a null attribute

        /// <summary>
        /// This method retrieves the first attribute of the specified type that is applied to the passed in member (field, property, enum instance, etc.).
        /// 
        /// This is the core method for retrieving attributes applied to code members. Other convenience methods build on top of this functionality.
        /// </summary>
        /// <param name="decoratedMember">The member (field, property, enum instance, etc.) from which the caller wishes to retrieve the requested attribute</param>
        /// <param name="soughtAttributeType">The type of attribute which the caller wishes to retrieve from the specified member</param>
        /// <returns>The first attribute of the specified type that is applied to the specified member. The attribute instance is weakly typed as object.</returns>
        private static object GetSingleMemberAttribute(MemberInfo decoratedMember, Type soughtAttributeType)
        {
            if (decoratedMember == null || soughtAttributeType == null)
            {
                throw new ArgumentException(
                    "Parameters MemberInfo decoratedMember and Type soughtAttributeType must both be assigned non-null values.");
            }

            object attribute = null;
            object[] attributes = decoratedMember.GetCustomAttributes(soughtAttributeType, false);

            if (attributes.Count() > 0)
            {
                attribute = attributes[0]; //there can only be one attrib of this type on any enum value
            }

            return attribute;
        }



        /// <summary>
        /// This method retrieves the first attribute of the specified type that is applied to the passed in enum instance.
        /// 
        /// This is a simple passthrough to GetEnumInstanceAttribute that does the strong casting of the returned attribute for convenience.
        /// </summary>
        /// <param name="enumInstance">The enum instance from which the caller wishes to retrieve the requested attribute</param>
        /// <typeparam name="T">The type of attribute which the caller wishes to retrieve from the specified enum instance</typeparam>
        /// <returns>The first attribute of the specified type that is applied to the specified enum instance</returns>
        private static T GetEnumInstanceAttribute<T>(Enum enumInstance) where T : Attribute
        {
            return (T)GetEnumInstanceAttribute(enumInstance, typeof(T));
        }

        //comes from parent declaration, not individual enum member

        /// <summary>
        /// This method retrieves the first attribute of the specified type that is applied to the parent enum definition which 
        /// contains the specified enum instance. That is, the caller passes in an enum instance, but the requested attribute is
        /// retrieved from the parent enum that defines the specified enum instance.
        /// 
        /// </summary>
        /// <typeparam name="T">The type of attribute which the caller wishes to retrieve from the parent enum definition which contains the specified enum instance</typeparam>
        /// <param name="enumInstance">The enum instance from whose parent enum definition the caller wishes to retrieve the requested attribute</param>
        /// <returns>The first attribute of the specified type that is applied to the parent enum definition which contains the specified enum instance</returns>
        private static T GetEnumDefinitionAttribute<T>(Enum enumInstance) where T : Attribute
        {
            if (enumInstance == null)
            {
                throw new ArgumentException("Parameter Enum enumInstance may not be null.");
            }

            var attribute = (T)GetSingleMemberAttribute(enumInstance.GetType(), typeof(T));

            return attribute;
        }
        #endregion

    }
}
